package com.atguigu.gulimail.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.gulimail.product.dao.CategoryBrandRelationDao;
import com.atguigu.gulimail.product.vo.CataLog2Vo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimail.product.dao.CategoryDao;
import com.atguigu.gulimail.product.entity.CategoryEntity;
import com.atguigu.gulimail.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationDao categoryBrandRelationDao;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedissonClient redissonClient;


    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        // 不用注入Dao
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
        //首先先找到所有1级分类
        List<CategoryEntity> collect = categoryEntities.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid().longValue() == 0;
        }).map(menu -> {
            menu.setChildren(getChildreds2(menu, categoryEntities));
            return menu;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());
        return collect;
    }


    private List<CategoryEntity> getChildreds2(CategoryEntity categoryEntity, List<CategoryEntity> all) {
        List<CategoryEntity> collect = all.stream().filter(menu -> {
            //当前菜单的父id等于指定菜单的id ，那么。当前菜单是指定菜单的子菜单
            if (menu.getCatLevel() > 3) {
                return false;
            } else {
                return menu.getParentCid() == categoryEntity.getCatId();
            }
        }).map(menu -> {
            menu.setChildren(getChildreds2(menu, all));
            return menu;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return collect;
    }

    //自定义批量删除 目录方法
    @Override
    public void removeMenuByIds(List<Long> asList) {
        //TODO   删除之前要先检查当前目录是否被其他目录使用。
        baseMapper.deleteBatchIds(asList);

    }


    //获取完整分类路径的方法
    @Override
    public Long[] getCatelogPath(Long categoryId) {
        List<Long> list = new ArrayList<>();
        list.add(categoryId);
        CategoryEntity byId = this.getById(categoryId);

        List<Long> categoryPath = getParentId(byId.getParentCid(), list);
        Collections.reverse(categoryPath);

        return categoryPath.toArray(new Long[categoryPath.size()]);

    }

    public List<Long> getParentId(Long categoryId, List<Long> list) {
        list.add(categoryId);
        CategoryEntity categoryEntity = this.getById(categoryId);
        if (categoryEntity != null) {
            Long parentCid = categoryEntity.getParentCid();
            if (parentCid != 0) {
                getParentId(parentCid, list);
            }
        }
        return list;
    }

    @Transactional
    @CacheEvict(value = {"catelog"}, allEntries = true)
    @Override
    public void updateDetil(CategoryEntity category) {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalog-update-lock");
        RLock rLock = readWriteLock.writeLock();
        try {
            rLock.lock();
            Thread.sleep(10000);
            this.updateById(category);
            if (!StringUtils.isEmpty(category.getName())) {
                //同步其他的表
                categoryBrandRelationDao.updateCategory(category.getCatId(), category.getName());
                //TODO 同步其他表
            }
        } catch (Exception e) {

        } finally {
            rLock.unlock();
        }
    }

    @Transactional
    @Cacheable(value = {"catelog"}, key = "#root.method.name")
    @Override
    public List<CategoryEntity> getCategory1Level() {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalog-update-lock");
        RLock rLock = readWriteLock.readLock();
        rLock.lock();
        try {
            System.out.println("查询一级分类....");
            long start = System.currentTimeMillis();
            List<CategoryEntity> categoryEntities = this.list(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
            //System.out.println("消耗时间"+(System.currentTimeMillis()-start));
            return categoryEntities;
        } finally {
            rLock.unlock();
        }
    }

    /**
     * 这是最新的写法  springCache整合redis
     *
     * @return
     */
    @Transactional
    @Cacheable(value = {"catelog"}, key = "#root.methodName")   //这个注解的意思是代表当前方法的结果需要缓存，如果缓存中有，就不调用方法。
    @Override
    public Map<String, List<CataLog2Vo>> getCataLogJson() {
        System.out.println("查询了数据库......");
        //查询所有
        List<CategoryEntity> list = this.list(null);
        //获取所有的1级分类的目录
        List<CategoryEntity> category1Level = getParent_cid(list, 0L);

        Map<String, List<CataLog2Vo>> collect = category1Level.stream().collect(Collectors.toMap(i -> i.getCatId().toString(), l1 -> {
            List<CategoryEntity> category2Entities = getParent_cid(list, l1.getCatId());
            List<CataLog2Vo> cataLog2Vos = null;
            if (category2Entities != null) {
                cataLog2Vos = category2Entities.stream().map(l2 -> {
                    CataLog2Vo cataLog2Vo = new CataLog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());
                    List<CategoryEntity> category3Entities = getParent_cid(list, l2.getCatId());
                    List<CataLog2Vo.CataLog3Vo> cataLog3Vos = null;
                    if (category3Entities != null) {
                        cataLog3Vos = category3Entities.stream().map(l3 -> {
                            CataLog2Vo.CataLog3Vo cataLog3Vo = new CataLog2Vo.CataLog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return cataLog3Vo;
                        }).collect(Collectors.toList());
                    }
                    cataLog2Vo.setCatalog3List(cataLog3Vos);

                    return cataLog2Vo;
                }).collect(Collectors.toList());
            }
            return cataLog2Vos;
        }));
        return collect;
    }

    @Override
    public List<CategoryEntity> listR() {
        //查询所有
        List<CategoryEntity> list = this.list(null);
        //查询所有 cateType为7的一级分类
        List<CategoryEntity> collect = list.stream().filter(v -> {
            return v.getParentCid() == 0 && v.getCateType() == 7;
        }).map(vo -> {
            vo.setChildren(getChildreds2(vo,list));
            return vo;
        }).collect(Collectors.toList());
        return collect;
    }


    /**
     * 这种是之前的写法。
     *
     * @return
     */
    public Map<String, List<CataLog2Vo>> getCataLogJson2() {
        // 先从redis缓存里判断key =catalogJson  是否存在
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");

        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存不命中，查询数据库。。。。。。");
            Map<String, List<CataLog2Vo>> cataLogJsonFromDB = getCataLogJsonFromDBWithRedissonLock();

            if (cataLogJsonFromDB == null) {
                // 如果查到的数据是null  那么就往里面放一个true把。  设置过期时间是3分钟  避免缓存穿透（查询一个不存在的数据）
                stringRedisTemplate.opsForValue().set("catalogJson", "000", 3L, TimeUnit.MINUTES);
            }

            return cataLogJsonFromDB;
        }
        System.out.println("缓存命中......直接返回");
        TypeReference<Map<String, List<CataLog2Vo>>> typeReference = new TypeReference<Map<String, List<CataLog2Vo>>>() {
        };
        Map<String, List<CataLog2Vo>> stringListMap = JSON.parseObject(catalogJson, typeReference);
        return stringListMap;

    }

    /**
     * 使用redisson 分布式锁
     */
    public Map<String, List<CataLog2Vo>> getCataLogJsonFromDBWithRedissonLock() {

        RLock lock = redissonClient.getLock("catelog-lock");
        lock.lock();
        Map<String, List<CataLog2Vo>> stringListMap = null;
        try {
            System.out.println("获取到锁。。" + Thread.currentThread().getId());
            stringListMap = getDataFromDB();
            return stringListMap;
        } finally {
            System.out.println("释放锁.." + Thread.currentThread().getId());
            lock.unlock();
        }

    }

    /**
     * 使用redisLock的方式
     *
     * @return
     */
    public Map<String, List<CataLog2Vo>> getCataLogJsonFromDBWithRedisLock() {
        //设置 set key value nx
        String uuid = UUID.randomUUID().toString();
        Boolean setOk = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300L, TimeUnit.SECONDS);
        //如果成果。那么当前微服务就占到了锁
        Map<String, List<CataLog2Vo>> stringListMap = null;
        if (setOk) {
            System.out.println("获取分布式锁成功/........");
            //执行业务逻辑
            //这种方式的弊端就是。 如果业务逻辑挂了。  那么锁就永远无法删除,
            //那么我给锁设置了自动过期时间了呢？
            //这种情况  加入我刚准备设置过期时间， 断电了。。 这也不行。
            //必须 设置锁和过期时间是原子性的。
            try {
                stringListMap = getDataFromDB();
            } finally {
                //使用lua脚本
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            //执行完毕之后删除锁。  如果业务时间过长。 删除之前锁就过期了  那么得设置uuid 只有是自己的锁才能删除
            // String lockValue = stringRedisTemplate.opsForValue().get("lock");
            //if(uuid.equals(lockValue)){
            //如果是自己的锁 才删除。     那么问题来了。从redis 从get("lock")也需要时间。。
            //如果刚匹配完成。自己的锁已经失效了，其他人占用了锁。那么这删的不就是其他人的锁。
            //所以 删锁也要保证原子性。
//                    stringRedisTemplate.delete("lock");
//                }
            //如何保证删锁的原子性。

            return stringListMap;
        } else {
            System.out.println("获取分布式锁失败........");
            //如果没占到锁 那么等200ms 我在去占锁
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCataLogJsonFromDBWithRedisLock();     //自旋的方式。
        }
    }


    public Map<String, List<CataLog2Vo>> getDataFromDB() {

        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
        if (!StringUtils.isEmpty(catalogJson)) {
            Map<String, List<CataLog2Vo>> stringListMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<CataLog2Vo>>>() {
            });
            return stringListMap;
        }
        System.out.println("查询了数据库......" + Thread.currentThread().getId());


        List<CategoryEntity> list = this.list(null);
        //获取所有的1级分类的目录
        List<CategoryEntity> category1Level = getParent_cid(list, 0L);

        Map<String, List<CataLog2Vo>> collect = category1Level.stream().collect(Collectors.toMap(i -> i.getCatId().toString(), l1 -> {
            List<CategoryEntity> category2Entities = getParent_cid(list, l1.getCatId());
            List<CataLog2Vo> cataLog2Vos = null;
            if (category2Entities != null) {
                cataLog2Vos = category2Entities.stream().map(l2 -> {
                    CataLog2Vo cataLog2Vo = new CataLog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());
                    List<CategoryEntity> category3Entities = getParent_cid(list, l2.getCatId());
                    List<CataLog2Vo.CataLog3Vo> cataLog3Vos = null;
                    if (category3Entities != null) {
                        cataLog3Vos = category3Entities.stream().map(l3 -> {
                            CataLog2Vo.CataLog3Vo cataLog3Vo = new CataLog2Vo.CataLog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return cataLog3Vo;
                        }).collect(Collectors.toList());
                    }
                    cataLog2Vo.setCatalog3List(cataLog3Vos);

                    return cataLog2Vo;
                }).collect(Collectors.toList());
            }
            return cataLog2Vos;
        }));
        //在锁里面直接把结果返回。
        //Redis里面存放的都是json串
        String s = JSON.toJSONString(collect);
        // 设置过期时间 防止缓存雪崩
        stringRedisTemplate.opsForValue().set("catalogJson", s, 1L, TimeUnit.DAYS);
        return collect;
    }


    /**
     * 当前方法是本地锁方式。
     *
     * @return
     */
    //从数据库获取
    public Map<String, List<CataLog2Vo>> getCataLogJsonFromDBWithLocal() {

        synchronized (this) {
            //所以这里再判定一遍的意义是getCataLogJsonFromDB（）方法调用完毕的时候，已经把数据保存到redis数据库中了。  保证只会查询一次数据库。
            String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
            if (!StringUtils.isEmpty(catalogJson)) {
                Map<String, List<CataLog2Vo>> stringListMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<CataLog2Vo>>>() {
                });
                return stringListMap;
            }
            System.out.println("查询了数据库......");

            /**
             * 1、将数据库的多次查询变为一次
             */
            return getDataFromDB();

        }

    }


    private List<CategoryEntity> getParent_cid(List<CategoryEntity> list, Long parent_cid) {
        List<CategoryEntity> collect = list.stream().filter(item -> item.getParentCid() == parent_cid).collect(Collectors.toList());
        return collect;
    }


}